import sys
import wx
import os

from hashlib import sha1
from time import strftime, localtime, time
from traceback import format_exc
from urllib import quote, unquote

from BitTornado.bencode import bencode, bdecode
from BitTornado.magnet import MAGNET_PREFIX, HashToMagnet, getHash, getArgument

from files import TorrentFiles
from connectmanager import TorrentConnections
from actions import TorrentActions
from config import TorrentConfig
from dialogs import TorrentDialogs
from status import TorrentStatus

from LMG.Utility.constants import *
from LMG.Utility.helpers import forceunicode, get_metainfo, fixInvalidName

from LMG.GUI.Base.ArtManager import ArtManager

################################################################

class FakeMetaData(dict):
    """
    A Fake metainfo data
    """
    def __init__(self, magnetURI):
        self['info'] = {}
        
        try:
            magnetURI = str(magnetURI)
        except UnicodeEncodeError:
            pass

        tracker = getArgument(magnetURI, 'tr')
        if tracker:
            self['announce'] = forceunicode(unquote(tracker))
        
        name = getArgument(magnetURI, 'dn')
        if name:
            self['info']['name'] = unquote(name) + ".torrent"
        else:
            self['info']['name'] = getHash(magnetURI, asHex = True) + ".torrent"
        
        self['info']['length'] = 1
        self['info']['piece length'] = 1024
        self['info']['pieces'] = " "*20

class Torrent(object):
    """
    Stores information about a torrent and keeps track of its status
    """
    def __init__(self, src = None, dest = None, forceasklocation = False, caller = ""):
        self.src = src
        # List values
        self.list = utility.window.list
        self.listbitmap = 0
        
        # Meta data
        if self.src.startswith(MAGNET_PREFIX):
            self.metainfo = FakeMetaData(self.src)
            self.infohash = getHash(self.src, asHex = True)
            self.src = self.src[len(MAGNET_PREFIX):]
            self.info = self.metainfo['info']
            self.rawinfo = ""
            self.hasMetadata = False
        else:
            self.metainfo = self.getResponse(force=True)
            if self.metainfo:
                self.infohash = sha1(bencode(self.metainfo['info'])).hexdigest()
                self.info = self.metainfo['info']
                self.rawinfo = bencode(self.metainfo['info'])
            self.hasMetadata = True
            
        if self.metainfo is None:
            return
        
        # Tracker List
        if 'announce-list' in self.metainfo:
            self.originalTrackers = self.metainfo['announce-list']
        elif 'announce' in self.metainfo:
            self.originalTrackers = [[self.metainfo['announce']]]
        else:
            self.originalTrackers = []
        self.trackerlist = self.originalTrackers

        # Torrent Handlers
        self.torrentconfig = TorrentConfig(self)
        self.status = TorrentStatus(self)
        self.actions = TorrentActions(self)
        self.dialogs = TorrentDialogs(self)
        self.connection = TorrentConnections(self)

        # Info section
        self.InitializeInfo()
        self.files.setupDest(dest, forceasklocation, caller)
        
    def InitializeInfo(self, info = None):
        if info is not None:
            torrentName = self.info['name']
            self.rawinfo = info
            self.info = self.metainfo['info'] = bdecode(info)
            
        # Meta data fixing: valid names, unicode names
        self.info['name'] = fixInvalidName(self.metainfo['info']['name'])              
        if 'files' in self.info:
            for index in range(len(self.info['files'])):
                temppath = self.info['files'][index]['path']
                self.info['files'][index]['path'] = [fixInvalidName(part) for part in temppath]
        self.metainfo = self.makeunicode(dict(self.metainfo))        
                  
        # Torrent Parameters
        self.addedTime = 0
        self.completedTime = 0
        if info is None:    # only apply once
            self.files = TorrentFiles(self)
            self.private = bool(self.info.get('private'))
            self.title = None
            self.prio = 2
            self.message = ""
            self.totalpeers = "?"
            self.totalseeds = "?"
        else:
            self.files.__init__(self)
            self.private = bool(self.info.get('private'))
            self.writeSrc(torrentName)
            self.hasMetadata = True
            self.torrentconfig.writeSrc()
            self.updateColumns(force = True)
            self.files.updateRealSize()

    def HasMetadata(self):
        return self.hasMetadata

    def writeSrc(self, torrentName):
        torrentsrc = os.path.join(utility.getConfigPath(), "torrent", torrentName)
        h = open(torrentsrc, 'wb')
        h.write(bencode(self.metainfo))
        h.close()
        self.src = torrentsrc
        return self.src
            
    def makeunicode(self, metadata):
        """
        Insures unicode support
        """
        # Change torrent name to unicode
        if 'name.utf-8' in metadata['info']:
            namekey = 'name.utf-8'
        else:
            namekey = 'name'
        if 'encoding' in metadata:
            encoding = metadata['encoding']
            try:
                metadata['info'][namekey] = metadata['info'][namekey].decode(encoding)
            except:
                metadata['info'][namekey] = forceunicode(metadata['info'][namekey])
        else:
            metadata['info'][namekey] = forceunicode(metadata['info'][namekey])

        if namekey != 'name':
            metadata['info']['name'] = metadata['info'][namekey]

        # Change all files to unicode
        if 'files' in self.info:
            numfiles = len(self.info['files'])
            if 'path.utf-8' in self.info['files'][0]:
                pathkey = 'path.utf-8'
            else:
                pathkey = 'path'
            for i in range(numfiles):
                for j in range(len(self.info['files'][i]['path'])):
                    self.info['files'][i]['path'][j] = forceunicode(self.info['files'][i][pathkey][j])

        return metadata
    
    def updatetrackerlist(self):        
        # if single tracker
        if len(self.trackerlist) == 1 and len(self.trackerlist[0]) == 1:
            self.metainfo['announce'] = self.trackerlist[0][0]
            if 'announce-list' in self.metainfo:
                del self.metainfo['announce-list']
        # if Multi tracker
        elif len(self.trackerlist) > 0:
            self.metainfo['announce-list'] = self.trackerlist
            if 'announce' in self.metainfo:
                self.metainfo['announce'] = self.trackerlist[0][0]
        # if no tracker, 
        else:
            if 'announce' in self.metainfo:
                del self.metainfo['announce']
            if 'announce-list' in self.metainfo:
                del self.metainfo['announce-list']            
            #self.trackerlist = self.originalTrackers   # ignore it and use the original tracker list

        if self.status.isActive(checking = False):
            self.connection.engine.dow.ChangeTrackerlist(self.trackerlist)
        self.updateColumns([COL_TRACKER])

    def getTracker(self):
        if self.status.isActive(checking = False) and \
           self.connection.engine.curtracker:
            return self.connection.engine.curtracker
        if self.trackerlist:
            return self.trackerlist[0][0]
        return ""

    def getMagnetLink(self):
        if not self.hasMetadata:
            return MAGNET_PREFIX + self.src
        magnet = MAGNET_PREFIX + HashToMagnet(self.infohash, asHex = True)
        title = self.getTitle(kind = "original")
        if title:
            magnet += "&dn=" + quote(title.encode("utf-8"))
        tracker = self.getTracker()
        if tracker:
            magnet += "&tr=" + quote(tracker.encode("utf-8"))
        return magnet
        
    def postInitTasks(self):        
        """
        Tasks to perform when first starting adding this torrent to the display
        """
        # Add a new item to the list
        self.listindex = self.list.GetItemCount()
        self.list.InsertStringItem(self.listindex, "")
        self.list.SetItemData(self.listindex, len(utility.torrents['all']))

        utility.torrents["all"].append(self)
        utility.torrents["inactive"][self] = 1
        
        # Read extra information about the torrent
        self.torrentconfig.readAll()

        # Set added time if needed
        if self.addedTime == 0:
            self.addedTime = time()
            
        # Allow updates
        self.status.dontupdate = False
        
        # Add Status info in List
        self.updateColumns(force = True)
        
        # Update the size to reflect torrents with pieces set to "download never"
        self.files.updateRealSize()
        
        # Do a quick check to see if it's finished
        self.status.isDoneUploading()

    def changeListIndex(self, listindex):
        self.listindex = listindex
        self.list.SetItemData(self.listindex, utility.torrents["all"].index(self))
           
    def getColumnValue(self, colid = None, default = 0.0):
        """
        As opposed to getColumnText,
        this will get numbers in their raw form for doing comparisons

        default is used when getting values for sorting comparisons
        (this way an empty string can be treated as less than 0.0)
        """
        if colid is None:
            colid = COL_TITLE
        value = None
        
        activetorrent = self.status.isActive(checking = False, pause = False)
        
        try:
            if colid == COL_PROGRESS: # Progress
                if self.status.isActive(pause = False):
                    progress = self.connection.engine.progress
                else:
                    progress = self.files.progress
                value = progress
    
            elif colid == COL_PRIO: # Priority
                value = self.prio
    
            elif colid == COL_SIZE: # Size
                if self.hasMetadata:
                    value = self.files.floattotalsize
                else:
                    value = self.files.getSize()
            elif colid == COL_DLSPEED: # DL Speed
                if activetorrent and not self.status.completed:
                    value = self.connection.engine.rate['down']
    
            elif colid == COL_ULSPEED: # UL Speed
                if activetorrent:
                    value = self.connection.engine.rate['up']
                    
            elif colid == COL_RATIO: # %U/D Size
                if self.files.downsize == 0.0 :
                    if self.files.floattotalsize != 0.0:
                        value = ((self.files.upsize/self.files.floattotalsize) * 100)
                    else:
                        value = 100
                else:
                    value = ((self.files.upsize/self.files.downsize) * 100)

            elif colid == COL_DONESIZE: # Done Size
                value = self.files.sizeDone
                    
            elif colid == COL_DLSIZE: # Download Size
                value = self.files.downsize
            
            elif colid == COL_ULSIZE: # Upload Size
                value = self.files.upsize

            elif colid == COL_ETA: # ETA
                if activetorrent:
                    if self.status.completed:
                        if self.connection.getTargetSeedingTime() == 0 and \
                           self.connection.getSeedOption('uploadratio') == "0":
                            value = 999999999999999
                        else:
                            value = self.connection.seedingtimeleft
                    elif self.connection.engine.eta is not None:
                        value = self.connection.engine.eta
            
            elif colid == COL_RESOURCES: # Seeds/Peers
                if activetorrent:
                    value = self.connection.engine.numconnections           

            elif colid == COL_ADDED_ON: # Added On
                value = self.addedTime

            elif colid == COL_COMPLETED_ON: # Completed On
                value = self.completedTime

            elif colid == COL_SEEDING_TIME:
                if self.status.completed:
                    value = self.connection.seedingtime
                    
            else:
                value = self.getColumnText(colid)
                try:
                    value = value.upper()
                except:
                    pass
        except:
            value = self.getColumnText(colid)
            try:
                value = value.upper()
            except:
                pass
            
        if value is None or value == "":
            return default
            
        return value
                
    def getColumnText(self, colid, force = False):
        """
        Get the text representation of a given column's data
        (used for display)
        """
        text = None
        
        activetorrent = self.status.isActive(checking = False, pause = False)
        
        try:
            if colid == COL_TITLE: # Title
                if self.title is None:
                    text = self.files.filename
                else:
                    text = self.title
                
            elif colid == COL_PROGRESS: # Progress
                if utility.config.Read("progress_bars", "int") != 0 and not force:
                    return ""
                progress = self.files.progress
                if self.status.isActive(pause = False):
                    progress = self.connection.engine.progress
                
                # Truncate the progress value rather than round down
                # (will show 99.9% for incomplete torrents rather than 100.0%)
                progress = int(progress * 10)/10.0
                
                text = ('%.1f' % progress) + "%"

            elif colid == COL_BTSTATUS: # BT Status
                text = self.status.getStatusText()

            elif colid == COL_PRIO: # Priority
                priorities = [ _('highest'), _('high'), _('normal'), _('low'), _('lowest') ]
                text = priorities[self.prio]

            elif colid == COL_SIZE: # Size
                if not self.hasMetadata:
                    text = utility.size_format(self.files.getSize()) 
                elif self.files.floattotalsize != self.files.realsize:                    
                    # Some file pieces are set to "download never"
                    label = utility.size_format(self.files.floattotalsize, textonly = True)
                    realsizetext = utility.size_format(self.files.realsize, truncate = 1, stopearly = label, applylabel = False)
                    totalsizetext = utility.size_format(self.files.floattotalsize, truncate = 1)
                    text = realsizetext + "/" + totalsizetext
                else:                    
                    text = utility.size_format(self.files.floattotalsize)                    
                    
            elif (colid == COL_DLSPEED
                  and activetorrent
                  and not self.status.completed): # DL Speed
                value = self.connection.engine.rate['down']
                text = utility.speed_format(value)

            elif colid == COL_ULSPEED and activetorrent: # UL Speed
                value = self.connection.engine.rate['up']
                text = utility.speed_format(value)

            elif colid == COL_RATIO: # %U/D Size
                if self.files.downsize == 0.0 :
                    if self.files.floattotalsize != 0.0:
                        ratio = ((self.files.upsize/self.files.floattotalsize) * 100)
                    else:
                        ratio = 100
                else:
                    ratio = ((self.files.upsize/self.files.downsize) * 100)
                text = '%.1f' % (ratio) + "%"

            elif colid == COL_MESSAGE: # Error Message
                text = self.message

            elif colid == COL_DONESIZE: # Done Size
                if self.hasMetadata:                    
                    text = utility.size_format(self.files.sizeDone)
                else:
                    text = utility.size_format(self.files.sizeDone_metadataless)
            elif colid == COL_DLSIZE: # Download Size
                text = utility.size_format(self.files.downsize)
                
            elif colid == COL_ULSIZE: # Upload Size
                text = utility.size_format(self.files.upsize)

            elif colid == COL_ETA and activetorrent: # ETA
                if self.status.completed:
                    if self.connection.getTargetSeedingTime() == 0 and \
                       self.connection.getSeedOption('uploadratio') == "0":
                        text = "(oo)"
                    else:
                        value = self.connection.seedingtimeleft
                        text = "(" + utility.eta_value(value) + ")"
                elif self.connection.engine.eta is not None:
                    value = self.connection.engine.eta
                    text = utility.eta_value(value)

            elif colid == COL_RESOURCES: # Seeds/Peers
                if activetorrent:
                    seeds = ('%d' % self.connection.engine.numseeds)
                    peers = ('%d' % self.connection.engine.numpeers)
                else:
                    seeds = "0"
                    peers = "0" 
                text =  seeds + "/" + peers + " [" + str(self.totalseeds) + "/" + str(self.totalpeers) + "]"

            elif colid == COL_TRACKER:  # Tracker
                text = self.getTracker()

            elif colid == COL_ADDED_ON: # Added On
                text = ""
                if self.addedTime:
                    text = strftime("%x %X", localtime(self.addedTime))
                    
            elif colid == COL_COMPLETED_ON: # Completed On
                text = ""
                if self.completedTime:
                    text = strftime("%x %X", localtime(self.completedTime))

            elif colid == COL_SEEDING_TIME: # Seeding time
                if self.status.completed:
                    text = utility.eta_value(self.connection.seedingtime)
                    
            else:
                text = ""
        except:
            exception_val = format_exc()
            if colid != COL_MESSAGE:
                self.changeMessage(exception_val, msg_type = "error")
            else:
                sys.stderr.write(exception_val)

        if text is None:
            text = ""
            
        return text

    def changeMessage(self, message = "", msg_type = "clear"):       
        # Clear the error message
        if msg_type == "clear":
            self.message = ""
            self.updateColumns([COL_MESSAGE])
            return
        
        if not message:
            return

        message = forceunicode(message)
        
        now = time()
        if msg_type == "error" or msg_type == "status":
            self.message = strftime('%H:%M', localtime(now)) + " - " + message
            self.updateColumns([COL_MESSAGE])

        if msg_type == "error":
            wx.LogError(self.getTitle() + " - " + message)
        else:
            wx.LogMessage(self.getTitle() + " - " + message)
        
    def updateColumns(self, columnlist = None, force = False):
        """
        Update multiple columns in the display
        if columnlist is None, update all columns
        (only visible columns will be updated)
        """
        if columnlist is None:
            columnlist = range(self.list.columns.minid, self.list.columns.maxid)

        try:
            for colid in columnlist:
                # Don't do anything if shutting down or minimized
                if self.status.dontupdate or not utility.frame.GUIupdate:
                    return

                # Only update if this column is currently shown
                rank = self.list.columns.getRankfromID(colid)
                if (rank == -1):
                    continue
                
                text = self.getColumnText(colid)
                
                self.list.setString(self.listindex, rank, text, force)

                if COL_BTSTATUS in columnlist:
                    self.updateStatusIcon(force)

                if COL_PROGRESS in columnlist:
                    self.updateProgressBar()                    
        except wx.PyDeadObjectError:
            pass
        
    def updateProgressBar(self):
        if 0 <= self.listindex < len(self.list.progressBars):
            self.list.progressBars[self.listindex].SetValue(self.getColumnValue(COL_PROGRESS))

    def updateStatusIcon(self, force = False):
        """
        Update Status Icon
        """
        value = self.status.value

        bmp = 0
        if self.status.isActive():
            if value == STATUS_PAUSE:
                bmp = ICON_PAUSED
            elif value == STATUS_SUPERSEED:
                bmp = ICON_SUPERSEED
            elif self.status.isCheckingOrAllocating():
                bmp = ICON_CHECKING
            elif self.connection.engine is not None:
                status = self.status.getStatusText()
                if status == _("working"):
                    bmp = ICON_WORKING
                elif status == _("seeding"):
                    bmp = ICON_SEEDING
                elif status == _("connecting"):
                    bmp = ICON_CONNECTING
        elif value == STATUS_QUEUE:
            if self.status.completed:
                bmp = ICON_COMPLETED_QUEUE
            else:
                bmp = ICON_QUEUE
        elif value == STATUS_FINISHED or self.status.completed:
            bmp = ICON_COMPLETED
        elif value == STATUS_STOP:
            bmp = ICON_STOPPED
        if self.listbitmap != bmp or force:
            self.listbitmap = bmp
            self.list.SetItemImage(self.listindex, bmp)
        
    def updateSingleItemStatus(self):
        """
        Update the fields that change frequently for active torrents
        """
        # Do check to see if we're done uploading
        self.status.isDoneUploading()
               
        self.updateColumns([COL_PROGRESS, 
                            COL_BTSTATUS, 
                            COL_ETA, 
                            COL_DLSPEED, 
                            COL_ULSPEED, 
                            COL_ULSIZE,
                            COL_DONESIZE,
                            COL_RESOURCES])
        
    def getResponse(self, force = False):
        """
        Get metainfo for the torrent
        """
        if not force and self.status.isActive():
            #active process
            metainfo = self.connection.engine.dow.getResponse()
        else:
            #not active process
            metainfo = get_metainfo(self.src)

        return metainfo
                      
    def makeInactive(self, update = True):          
        self.files.updateProgress()

        if self.status.value == STATUS_HASHCHECK:
            self.status.updateStatus(self.actions.oldstatus)
        elif self.status.value == STATUS_STOP:
            pass
        elif self.connection.engine is not None:
            # Ensure that this part only gets called once
            self.status.updateStatus(STATUS_QUEUE)
           
        # Write out to config
        self.torrentconfig.writeAll()
           
        if update:
            self.updateSingleItemStatus()

    def getTitle(self, kind = "current"):
        
        if kind == "original":
            title = self.metainfo['info']['name']
        elif kind == "torrent":
            torrentfilename = os.path.split(self.src)[1]
            torrentfilename = torrentfilename[:torrentfilename.rfind('.torrent')]
            title = torrentfilename
        elif kind == "dest":
            if self.files.isFile():
                destloc = self.files.dest
            else:
                destloc = self.files.getProcDest(pathonly = True, checkexists = False)
            title = os.path.split(destloc)[1]
        else: # current
            title = self.getColumnText(COL_TITLE)
            
        return title

    def changeTitle(self, title):
        if title == self.files.filename:
            self.title = None
        else:
            self.title = title
        
        self.torrentconfig.writeNameParams()
        
        self.updateColumns([COL_TITLE])
        
        details = utility.window.details
        if details.getTorrent() == self:
            try:
                details.updateGeneral()
            except wx.PyDeadObjectError:
                pass
        
            
    def changePriority(self, prio):
        """
        Change the priority for the torrent
        """
        if prio > 4:
            prio = 4
        if prio < 0:
            prio = 0
        
        self.prio = prio
        self.updateColumns([COL_PRIO])
        self.torrentconfig.writePriority()
       
    def shutdown(self):
        """
        Things to do when shutting down a torrent
        """
        # Set shutdown flag to true
        self.status.dontupdate = True
        
        self.torrentconfig.writeAll()
        
        self.connection.stopEngine()
        
        del utility.torrents["inactive"][self]

    def getDetailsString(self):
        """
        Return a string with minimal info
        """
        
        message =  _("Status: ") + self.getColumnText(COL_BTSTATUS) + "\n"
        message += _("Progress: ") + self.getColumnText(COL_PROGRESS, True) + "\n"
        message += _("Ratio: ") + self.getColumnText(COL_RATIO)


        if self.status.isActive(checking = False, pause = False):
            message += '\n' + _("Upload: %s at %s") % (self.getColumnText(COL_ULSIZE),
                                                       self.getColumnText(COL_ULSPEED))
            if not self.status.completed:
                message += '\n' + _("Download: %s of %s at %s") % (self.getColumnText(COL_DONESIZE),
                                                                   self.getColumnText(COL_SIZE),
                                                                   self.getColumnText(COL_DLSPEED))

        if self.getColumnText(COL_ETA):
            message += "\n" + _("ETA: ") + self.getColumnText(COL_ETA)
        if self.message:
            message += "\n" + self.message

        return message
    
    def getInfo(self, fieldlist = None):
        """
        Get information about the torrent to return to the webservice
        """
        # Default to returning all fields
        if fieldlist is None:
            fieldlist = range(self.list.columns.minid, self.list.columns.maxid)

        try :
            retmsg = ""

            for colid in fieldlist:
                retmsg += self.getColumnText(colid) + "|"
                       
            retmsg += self.infohash + "\n"
            
            return retmsg
        except:               
            # Should never get to this point
            return "|" * len(fieldlist) + "\n"

    def updateScrapeData(self, newpeer, newseed, message = "", messagetype = "status"):
        """
        Update the torrent with new scrape information
        """
        self.actions.lastgetscrape = time()
        self.totalpeers = newpeer
        self.totalseeds = newseed
        self.updateColumns([COL_RESOURCES])

        detailwin = utility.window.details
        if detailwin.getTorrent() == self and detailwin.IsShown() and detailwin.networkPage.IsShown():
            detailwin.updateNetwork('transfer')
            
        if message != "":
            if message == _('Scraping'):
                msgtype = "status"
            elif message == _('Scraping Done'):
                msgtype = "status"
                message += ": %s seeds %s peers" % (self.totalseeds, self.totalpeers)
            else:
                msgtype = "error"
             
            self.changeMessage(message, msgtype)

        # Auto Start
        self.autoStartCheck()
        
    def autoStartCheck(self):
        try:
            totalseeds = int(self.totalseeds)
        except:
            return False
        
        if not utility.config.Read('autostart_scrape', "boolean"):
            return False

        if self.status.value == STATUS_QUEUE:
            if self.status.completed and totalseeds < utility.config.Read('autostart_scrape_value', "int"):
               if self.actions.resume():
                   self.status.autostarted = True
                   
        elif self.status.isActive(checking = False) and self.status.autostarted:
            if totalseeds >= utility.config.Read('autostart_scrape_value', "int"):
                self.actions.queue()
                self.status.autostarted = False
            
                
